package com.dinuscxj.shootrefreshview;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.Shader;
import android.graphics.SweepGradient;
import android.util.AttributeSet;
import android.util.Property;
import android.view.View;
import android.view.animation.LinearInterpolator;
public class ShootRefreshView extends View implements IRefreshStatus {
private static final int DEFAULT_STROKE_COLOR = Color.parseColor("#ffc6c6c6");
private static final int DEFAULT_GRADIENT_START_COLOR = Color.parseColor("#ffababab");
private static final int DEFAULT_GRADIENT_END_COLOR = Color.parseColor("#0dababab");
private static final int DEGREE_60 = 60;
private static final int DEGREE_360 = 360;
private static final int START_ANGLE = -90;
private static final int PRE_SHOOT_LINE_TOTAL_ROTATE_DURATION = 10000;
private static final int SHOOT_LINE_ROTATE_DURATION = 5000;
private static final int SHOOT_LINE_STRETCH_DURATION = 500;
private static final int OUT_RING_ROTATE_DURATION = 500;
private static final int TOTAL_DURATION = PRE_SHOOT_LINE_TOTAL_ROTATE_DURATION +
SHOOT_LINE_ROTATE_DURATION + SHOOT_LINE_STRETCH_DURATION;
private static final float PRE_SHOOT_LINE_TOTAL_ROTATE_END_OFFSET =
(float) PRE_SHOOT_LINE_TOTAL_ROTATE_DURATION / (float) TOTAL_DURATION;
private static final float SHOOT_LINE_ROTATE_END_OFFSET = PRE_SHOOT_LINE_TOTAL_ROTATE_END_OFFSET +
(float) SHOOT_LINE_ROTATE_DURATION / (float) TOTAL_DURATION;
private static final float SHOOT_LINE_STRETCH_END_OFFSET = 1.0f;
private static final float SHOOT_LINE_ROTATE_END_RADIANS = (float) (Math.PI / 6.0);
private static final float SHOOT_LINE_ROTATE_START_RADIANS = (float) (Math.PI / 2.5);
private static final float SHOOT_LINE_ROTATE_START_DEGREE =
(float) Math.toDegrees(SHOOT_LINE_ROTATE_END_RADIANS);
private static final float INTERVAL_RADIANS = (float) (Math.PI / 3.0);
private static final float SQRT_3 = (float) Math.sqrt(3.0);
private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final RectF mBounds = new RectF();
private int mRadius;
private int mCenterX;
private int mCenterY;
private int mStrokeColor;
private int mGradientStartColor;
private int mGradientEndColor;
private int mStrokeWidth;
private float mOutRingRotateAngle;
private float mShootLineRotateRadians;
private float mShootLineTotalRotateAngle;
private Shader mRefreshingShader;
private ValueAnimator mPreShootLineTotalRotateAnimator;
private ValueAnimator mShootLineRotateAnimator;
private ValueAnimator mShootLineStretchAnimator;
private ValueAnimator mOutRingRotateAnimator;
public static final Property<ShootRefreshView, Float> SHOOT_LINE_ROTATE_RADIANS =
new Property<ShootRefreshView, Float>(Float.class, null) {
@Override
public Float get(ShootRefreshView object) {
return object.mShootLineRotateRadians;
}
@Override
public void set(ShootRefreshView object, Float value) {
object.mShootLineRotateRadians = value;
object.invalidate();
}
};
public static final Property<ShootRefreshView, Float> SHOOT_LINE_TOTAL_ROTATE_DEGREE =
new Property<ShootRefreshView, Float>(Float.class, null) {
@Override
public Float get(ShootRefreshView object) {
return object.mShootLineTotalRotateAngle;
}
@Override
public void set(ShootRefreshView object, Float value) {
object.mShootLineTotalRotateAngle = value;
object.invalidate();
}
};
public ShootRefreshView(Context context) {
this(context, null);
}
public ShootRefreshView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ShootRefreshView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
resolveAttrs(context, attrs);
initPaint();
initAnimator();
reset();
}
private void resolveAttrs(Context context, AttributeSet attrs) {
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ShootRefreshView);
mStrokeColor = ta.getColor(R.styleable.ShootRefreshView_strokeColor,
DEFAULT_STROKE_COLOR);
mGradientStartColor = ta.getColor(R.styleable.ShootRefreshView_gradientStartColor,
DEFAULT_GRADIENT_START_COLOR);
mGradientEndColor =
ta.getColor(R.styleable.ShootRefreshView_gradientEndColor, DEFAULT_GRADIENT_END_COLOR);
mStrokeWidth =
ta.getDimensionPixelSize(R.styleable.ShootRefreshView_strokeWidth,
(int) DensityUtil.dp2px(getContext(), 1.0f));
ta.recycle();
mRefreshingShader = new SweepGradient(0, 0, new int[]{mGradientStartColor, mGradientEndColor},
new float[]{0.0f, 1.0f});
}
private void initPaint() {
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(mStrokeWidth);
mPaint.setColor(mStrokeColor);
}
private void initAnimator() {
//Note: the following uses the 'kwai Line' represent the six lines of the shutter
//Step1: Rotate the 'kwai Line', but the shutter does not open
mPreShootLineTotalRotateAnimator =
ValueAnimator.ofFloat(-(SHOOT_LINE_ROTATE_START_DEGREE / 2.0f) - 240.0f,
-(SHOOT_LINE_ROTATE_START_DEGREE / 2.0f) - 120.0f);
mPreShootLineTotalRotateAnimator.setInterpolator(new LinearInterpolator());
mPreShootLineTotalRotateAnimator.setDuration(PRE_SHOOT_LINE_TOTAL_ROTATE_DURATION);
mPreShootLineTotalRotateAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mShootLineTotalRotateAngle = (float) animation.getAnimatedValue();
invalidate();
}
});
//Step 2: Rotate the 'Kwai Line' and open the shutter
PropertyValuesHolder shootLineIntersectHolder = PropertyValuesHolder
.ofFloat(SHOOT_LINE_ROTATE_RADIANS, SHOOT_LINE_ROTATE_START_RADIANS,
SHOOT_LINE_ROTATE_END_RADIANS);
PropertyValuesHolder shootLineTotalRotateAnimatorHolder = PropertyValuesHolder
.ofFloat(SHOOT_LINE_TOTAL_ROTATE_DEGREE, -(SHOOT_LINE_ROTATE_START_DEGREE / 2.0f) - 120.0f,
-(SHOOT_LINE_ROTATE_START_DEGREE / 2.0f));
mShootLineRotateAnimator = ObjectAnimator.ofPropertyValuesHolder(this,
shootLineIntersectHolder, shootLineTotalRotateAnimatorHolder);
mShootLineRotateAnimator.setInterpolator(new LinearInterpolator());
mShootLineRotateAnimator.setDuration(SHOOT_LINE_ROTATE_DURATION);
//Step3: Take the center of the 'Kwai Line' as the base point, and zoom 'Kwai Line'
mShootLineStretchAnimator = ValueAnimator.ofFloat(SHOOT_LINE_ROTATE_END_RADIANS, 0);
mShootLineStretchAnimator.setInterpolator(new LinearInterpolator());
mShootLineStretchAnimator.setDuration(SHOOT_LINE_STRETCH_DURATION);
mShootLineStretchAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mShootLineRotateRadians = (Float) animation.getAnimatedValue();
mShootLineTotalRotateAngle = -(float) (Math.toDegrees(mShootLineRotateRadians) / 2.0f);
invalidate();
}
});
//Step4: Perform a refresh animation, rotate the gradient ring
mOutRingRotateAnimator = ValueAnimator.ofFloat(0, DEGREE_360);
mOutRingRotateAnimator.setRepeatCount(ValueAnimator.INFINITE);
mOutRingRotateAnimator.setInterpolator(new LinearInterpolator());
mOutRingRotateAnimator.setDuration(OUT_RING_ROTATE_DURATION);
mOutRingRotateAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mOutRingRotateAngle = (float) animation.getAnimatedValue();
invalidate();
}
});
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawOuterRing(canvas);
drawShootLine(canvas);
}
private void drawOuterRing(Canvas canvas) {
canvas.save();
canvas.translate(mCenterX, mCenterY);
if (mOutRingRotateAnimator.isRunning()) {
canvas.rotate(START_ANGLE + mOutRingRotateAngle);
if (mPaint.getShader() != mRefreshingShader) {
mPaint.setShader(mRefreshingShader);
}
} else {
mPaint.setShader(null);
}
canvas.drawCircle(0.0f, 0.0f, mRadius, mPaint);
canvas.restore();
}
private void drawShootLine(Canvas canvas) {
if (mShootLineRotateRadians <= 0.0f || mOutRingRotateAnimator.isRunning()) {
return;
}
mPaint.setShader(null);
canvas.save();
canvas.translate(mCenterX, mCenterY);
canvas.rotate(mShootLineTotalRotateAngle);
for (int i = 0; i < 6; i++) {
canvas.save();
canvas.rotate(-DEGREE_60 * i);
if (mShootLineRotateRadians > SHOOT_LINE_ROTATE_END_RADIANS) {
double tanRotateAngle = Math.tan(mShootLineRotateRadians);
double tanRotateAngleOffset60 = Math.tan(mShootLineRotateRadians + INTERVAL_RADIANS);
//The intersection formula of 'Kwai Line'
float stopX = (float) ((1.0 - SQRT_3 * tanRotateAngleOffset60)
/ (2.0 * (tanRotateAngle - tanRotateAngleOffset60))) * mRadius;
float stopY = (float) ((2.0 * tanRotateAngleOffset60 - tanRotateAngle
- SQRT_3 * tanRotateAngle * tanRotateAngleOffset60)
/ (2.0 * (tanRotateAngle - tanRotateAngleOffset60))) * mRadius;
//Note: (0, -Radius) is Y-axis negative direction
canvas.drawLine(0, -mRadius, stopX, stopY, mPaint);
} else {
double tanRotateAngle = Math.tan(mShootLineRotateRadians);
//The zoom formula of 'Kwai Line'
float stopX =
(float) (2 * tanRotateAngle * mRadius / (Math.pow(tanRotateAngle, 2.0) + 1.0));
float stopY = (float) ((Math.pow(tanRotateAngle, 2.0) - 1.0) * mRadius
/ (Math.pow(tanRotateAngle, 2.0) + 1.0));
canvas.drawLine(0, -mRadius, stopX, stopY, mPaint);
}
canvas.restore();
}
canvas.restore();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mBounds.set(0 + getPaddingLeft(), 0 + getPaddingTop(), w - getPaddingRight(),
h - getPaddingBottom());
mBounds.inset(mStrokeWidth, mStrokeWidth);
mRadius = (int) (Math.min(mBounds.width(), mBounds.height()) / 2);
mCenterX = (int) mBounds.centerX();
mCenterY = (int) mBounds.centerY();
}
@Override
protected void onDetachedFromWindow() {
reset();
super.onDetachedFromWindow();
}
@Override
public void reset() {
mOutRingRotateAnimator.cancel();
mShootLineRotateRadians = SHOOT_LINE_ROTATE_START_RADIANS;
mShootLineTotalRotateAngle = -(SHOOT_LINE_ROTATE_START_DEGREE / 2.0f) - 240.0f;
mOutRingRotateAngle = 0.0f;
invalidate();
}
@Override
public void refreshing() {
mOutRingRotateAngle = 0.0f;
mShootLineTotalRotateAngle = 0.0f;
mShootLineRotateRadians = 0.0f;
if (mOutRingRotateAnimator.isRunning()) {
mOutRingRotateAnimator.cancel();
}
mOutRingRotateAnimator.start();
}
@Override
public void refreshComplete() {
}
@Override
public void pullToRefresh() {
}
@Override
public void releaseToRefresh() {
}
@Override
public void pullProgress(float pullDistance, float pullProgress) {
pullProgress = Math.min(1.0f, Math.max(0.0f, pullProgress));
if (pullProgress < PRE_SHOOT_LINE_TOTAL_ROTATE_END_OFFSET) {
mPreShootLineTotalRotateAnimator.setCurrentPlayTime((long) (pullProgress
/ PRE_SHOOT_LINE_TOTAL_ROTATE_END_OFFSET * PRE_SHOOT_LINE_TOTAL_ROTATE_DURATION));
} else if (pullProgress < SHOOT_LINE_ROTATE_END_OFFSET) {
mShootLineRotateAnimator.setCurrentPlayTime(
(long) ((pullProgress - PRE_SHOOT_LINE_TOTAL_ROTATE_END_OFFSET)
/ (SHOOT_LINE_ROTATE_END_OFFSET - PRE_SHOOT_LINE_TOTAL_ROTATE_END_OFFSET)
* SHOOT_LINE_ROTATE_DURATION));
} else {
if (pullProgress == 1.0f) {
mShootLineStretchAnimator.setCurrentPlayTime(SHOOT_LINE_STRETCH_DURATION);
} else {
mShootLineStretchAnimator.setCurrentPlayTime(
(long) ((pullProgress - SHOOT_LINE_ROTATE_END_OFFSET)
/ (SHOOT_LINE_STRETCH_END_OFFSET - SHOOT_LINE_ROTATE_END_OFFSET)
* SHOOT_LINE_STRETCH_DURATION));
}
}
}
}